package song;

import drumkit.Drumkit;
import drumkit.PlayingStyle;
import options.Options;

import javax.sound.midi.*;
import java.io.File;
import java.io.IOException;
import java.util.Random;

/**
 * Created by IntelliJ IDEA.
 * User: Aleksey
 * Date: 02.01.2008
 * Time: 18:49:37
 */
public class Song {
    private DrumTrack drumTrack;
    private Drumkit drumkit;

    public Song(Drumkit drumkit) {
        this.drumkit = drumkit;
        createDrumTrack();
    }

    private void createDrumTrack() {
        drumTrack = new DrumTrack();
        for (int i = 0; i < drumkit.getDrumCount(); i++) {
            for (int j = 0; j < drumkit.getDrum(i).getPositionCount(); j++) {
                drumTrack.addSubTrack(new DrumSubTrack(drumkit.getDrum(i), j));
            }
        }
        addBar(Options.DEFAULT_TEMPO, Options.DEFAULT_TIME_SIGNATURE);
    }

    public void copyBarToAnother(Integer index1, Integer index2) {
        for (int i = 0; i < getTrackCount(); i++) {
            getBar(i, index2).initialize(getBar(i, index1));
        }
        drumTrack.getDownbeat(index2).initialize(drumTrack.getDownbeat(index1));
    }

    public void deleteBar(Integer index) {
        if (getBarCount() < 2) {
            return;
        }
        for (int i = 0; i < getTrackCount(); i++) {
            getTrack(i).deleteBar(index);
        }
        drumTrack.deleteDownbeat(index);
    }

    public void insertBar(Integer index, Integer tempo, TimeSignature timeSignature) {
        for (int i = 0; i < drumTrack.getSubTrackCount(); i++) {
            DrumSubTrack subTrack = drumTrack.getSubTrack(i);
            Bar bar = new Bar(tempo, timeSignature);
            subTrack.insertBar(index, bar);
        }
        drumTrack.insertDownbeat(index, new Bar(tempo, timeSignature));
    }

    public void addBar(Integer tempo, TimeSignature timeSignature) {
        for (int i = 0; i < drumTrack.getSubTrackCount(); i++) {
            DrumSubTrack subTrack = drumTrack.getSubTrack(i);
            Bar bar = new Bar(tempo, timeSignature);
            subTrack.addBar(bar);
        }
        drumTrack.addDownbeat(new Bar(tempo, timeSignature));
    }

    public void setTempo(Integer barIndex, Integer tempo) {
        for (int i = 0; i < drumTrack.getSubTrackCount(); i++) {
            DrumSubTrack subTrack = drumTrack.getSubTrack(i);
            subTrack.getBar(barIndex).setTempo(tempo);
        }
        drumTrack.getDownbeat(barIndex).setTempo(tempo);
    }

    public void setTimeSignature(Integer barIndex, TimeSignature timeSignature) {
        for (int i = 0; i < drumTrack.getSubTrackCount(); i++) {
            DrumSubTrack subTrack = drumTrack.getSubTrack(i);
            subTrack.getBar(barIndex).setTimeSignature(timeSignature);
        }
        drumTrack.getDownbeat(barIndex).setTimeSignature(timeSignature);
    }

    public Integer getTrackCount() {
        return drumTrack.getSubTrackCount();
    }

    public Integer getBarCount() {
        return drumTrack.getSubTrack(0).getBarCount();
    }

    public DrumSubTrack getTrack(Integer index) {
        return drumTrack.getSubTrack(index);
    }

    public Bar getBar(Integer trackIndex, Integer barIndex) {
        return drumTrack.getSubTrack(trackIndex).getBar(barIndex);
    }

    public Bar getDownbeat(Integer barIndex) {
        return drumTrack.getDownbeat(barIndex);
    }

    public void saveToFile(String path, String songName) {
        try {
            applyVelocities();
            TempoTrack tempoTrack = new TempoTrack();

            Integer resolution = Options.RESOLUTION; // Ticks per quarter note
            Sequence seq = new Sequence(Sequence.PPQ, resolution);
            Track track = seq.createTrack();

            ShortMessage sm;
            MidiEvent me;
            MetaMessage mm;

            if (drumTrack.getSubTrackCount() == 0) {
                return;
            }

            Integer timeShift = 0;
            TimeSignature ts = new TimeSignature(false);
            Integer tmp = 0;
            for (int i = 0; i < drumTrack.getSubTrack(0).getBarCount(); i++) {
                if (i == 0 || !ts.getBeatNumber().equals(drumTrack.getSubTrack(0).getBar(i).getTimeSignature().getBeatNumber()) ||
                        !ts.getBeatUnit().equals(drumTrack.getSubTrack(0).getBar(i).getTimeSignature().getBeatUnit())) {
                    ts = drumTrack.getSubTrack(0).getBar(i).getTimeSignature();

                    // TIME SIGNATURE
                    byte beatNumber = ts.getBeatNumber().byteValue();
                    byte beatUnit = ts.getBeatUnit().byteValue(); // 2^2 = 4
                    byte metronome = 24; // 24 midi ticks in 1 quarter note (beat) - constant for us
                    byte numOf32dsInBeat = 8; // 8 32ds in quarter note - constant for us

                    mm = new MetaMessage();
                    mm.setMessage(88, new byte[]{beatNumber, beatUnit, metronome, numOf32dsInBeat}, 4);
                    me = new MidiEvent(mm, timeShift);
                    track.add(me);

                    tempoTrack.addSignatureEvent(timeShift, ts.getBeatNumber(), ts.getFullBeatUnit());
                }
                if (i == 0 || !tmp.equals(drumTrack.getSubTrack(0).getBar(i).getTempo())) {
                    // TEMPO
                    tmp = drumTrack.getSubTrack(0).getBar(i).getTempo();
                    Double bpm = tmp.doubleValue();
                    Integer tempo = new Double(60 / bpm * 1e6).intValue();// in microseconds per quarter-note

                    byte bt[] = new byte[3];
                    bt[2] = (byte) (tempo % 256);
                    tempo /= 256;
                    bt[1] = (byte) (tempo % 256);
                    tempo /= 256;
                    bt[0] = (byte) (tempo % 256);

                    mm = new MetaMessage();
                    mm.setMessage(81, new byte[]{bt[0], bt[1], bt[2]}, 3);
                    me = new MidiEvent(mm, timeShift);
                    track.add(me);

                    tempoTrack.addTempoEvent(timeShift, 60.0 / bpm);
                }

                Random random = new Random();
                for (int j = 0; j < drumTrack.getSubTrackCount(); j++) {
                    DrumSubTrack subTrack = drumTrack.getSubTrack(j);
                    Bar bar = subTrack.getBar(i);
                    for (int k = 0; k < bar.getPartCount(); k++) {
                        BarPart barPart = bar.getPart(k);
                        if (!barPart.getNote()) {
                            continue;
                        }
                        Integer relativeShift = bar.getRelativeTimeShift(k);

                        PlayingStyle style = subTrack.getDrum().getPosition(subTrack.getPosition()).getStyle(barPart.getStyle());
                        Integer key = Options.RANDOMIZE_KEYS ? style.getKey(random.nextInt(style.getKeyCount())) : style.getKey(0);

                        sm = new ShortMessage();
                        sm.setMessage(ShortMessage.NOTE_ON, key, barPart.getVelocity());
                        me = new MidiEvent(sm, timeShift + relativeShift);
                        track.add(me);

                        sm = new ShortMessage();
                        sm.setMessage(ShortMessage.NOTE_OFF, key, 0);
                        me = new MidiEvent(sm, timeShift + relativeShift + 1);
                        track.add(me);
                    }
                }
                timeShift += drumTrack.getSubTrack(0).getBar(i).getFullBarTime();
            }
            MidiSystem.write(seq, MidiSystem.getMidiFileTypes(seq)[0], new File(path + songName + ".mid"));
            tempoTrack.saveToFile(path, songName);
            
        }
        catch (InvalidMidiDataException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void loadFromFile(String fileName) {
        try {
            Sequence seq = MidiSystem.getSequence(new File(fileName));
            Track track = seq.getTracks()[0];
            for (int i = 0; i < track.size(); i++) {
                MidiMessage mm = track.get(i).getMessage();
                System.out.println("Event, tick: " + track.get(i).getTick());
                if (mm instanceof ShortMessage) {
                    ShortMessage sm = (ShortMessage) mm;
                    System.out.println("Short Message");
                    System.out.println(sm.getCommand());
                    System.out.println(sm.getChannel());
                    System.out.println(sm.getData1());
                    System.out.println(sm.getData2());
                }
                else
                if (mm instanceof MetaMessage) {
                    MetaMessage mms = (MetaMessage) mm;
                    System.out.println("Meta Message");
                    System.out.println(mms.getType());
                    for (int j = 0; j < mms.getData().length; j++) {
                        System.out.println(mms.getData()[j]);
                    }
                }
            }
        }
        catch (InvalidMidiDataException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void applyVelocities() {
        for (int i = 0; i < getBarCount(); i++) {
            for (int j = 0; j < getTrackCount(); j++) {
                Integer loudness = getBar(j, i).getLoudness();
                for (int k = 0; k < getBar(j, i).getPartCount(); k++) {
                    Integer velocity = 0;
                    if (getBar(j, i).getPart(k).getNote()) {
                        Integer time = getBar(j, i).getRelativeTimeShift(k);
                        Boolean strong = false;
                        for (int q = 0; q < getDownbeat(i).getPartCount(); q++) {
                            if (getDownbeat(i).getPart(q).getNote() && time >= getDownbeat(i).getRelativeTimeShift(q) && (q == getDownbeat(i).getPartCount() - 1 || time < getDownbeat(i).getRelativeTimeShift(q + 1))) {
                                strong = true;
                            }
                        }
                        velocity = strong ? Options.DOWNBEATS[loudness] : Options.UPBEATS[loudness];
                    }
                    getBar(j, i).getPart(k).setVelocity(velocity);
                }
            }
        }
    }
}
